<?php
/**
 * Represents a SleekPHP XML Page
 * Is used to render pretty much everything you see
 */
class Page {
	/**
	 * The XML code that makes up this page
	 *
	 * @var string $xml
	 */
	private $xml;

	/**
	 * Name of the root module/node in this page
	 *
	 * @var string $rootModule
	 */
	private $rootModule;

	/**
	 * DOMDocument node of root module
	 *
	 * @var string $rootNode
	 */
	private $rootNode;

	/**
	 * Creates the page XML (based on $xml), dom document etc
	 *
	 * @param string $xml
	 */
	public function __construct ($xml) {
		if (strpos($xml, '<') !== 0) {
			throw new Exception("\"$xml\" does not appear to be valid SleekPHP Page XML. Check your site's " . SleekPHP::currentPage() . ' page.');
		}

		# Save the XML that makes up this page
		$this->xml = str_replace(array("\n", "\t", "\r"), '', $xml);

		# Work out the root node / first element in XML
		preg_match('/<(\w+).*?>/', $xml, $matches);

		$this->rootModule = $matches[1];

		# Save an instance of DOMDocument and load it with our XML
		$this->domDoc = new DOMDocument();

		$this->domDoc->loadXML($this->xml);

		# Save a DOMDocument Element instance of the root node in the page
		$this->rootNode = $this->domDoc->getElementsByTagName($this->rootModule);
	}

	/**
	 * Checks the (potential) auth level of this page
	 */
	public function authLevel () {
		foreach ($this->rootNode as $module) {
			$al = $module->getAttribute('auth-level');

			return empty($al) ? AUTH_LEVEL_GUEST : $al;
		}
	}

	/**
	 * Checks the (potential) style for this page
	 */
	public function getStyle () {
		foreach ($this->rootNode as $module) {
			$style = $module->getAttribute('style');

			return empty($style) ? false : $style;
		}
	}

	/**
	 * Creates every module in this page
	 */
	public function init () {
		return $this->initModules($this->rootNode);
	}

	/**
	 * Creates all $modules
	 *
	 * @param array $modules
	 */
	private function initModules ($modules) {
		foreach ($modules as $module) {
			$this->initModule($module->nodeName);

			if ($module->hasChildNodes()) {
				$this->initModules($module->childNodes);
			}
		}

		return true;
	}

	private function initModule ($module) {
		$moduleClassName = SleekPHP::site()->getModuleClassName($module);

		if ($moduleClassName) {
			SleekPHP::$modules[$module] = new $moduleClassName($module);

			Logger::log("Module \"$module\" has been created.", __METHOD__);
		}
		else {
			Logger::log("Module \"$module\" does not have any class.", __METHOD__, Logger::WARNING);
		}
	}

	/**
	 * Runs every module in this page
	 *
	 * @param array $params
	 */
	public function run ($params = array()) {
		return $this->runModules($this->rootNode, $params);
	}

	/**
	 * Runs all $modules and passes them $params
	 *
	 * @param array $modules
	 * @param array $params
	 */
	private function runModules ($modules, $params = array()) {
		$return = null;

		foreach ($modules as $module) {
			$return = $this->runModule($module->nodeName, $params);

			if (is_string($return)) {
				return $return;
			}

			if ($module->hasChildNodes()) {
				$return = $this->runModules($module->childNodes, $params);

				if (is_string($return)) {
					return $return;
				}
			}
		}

		return true;
	}

	/**
	 * Excecutes $module's run()-method passing in $params
	 *
	 * @param string $module
	 * @param array $params
	 */
	private function runModule ($module, $params = array()) {
		if (isset(SleekPHP::$modules[$module])) {
			$return = SleekPHP::$modules[$module]->run($params);

			Logger::log("Module \"$module\" has run.", __METHOD__);

			return $return;
		}
	}

	/**
	 * Renders every module in this page's view
	 */
	public function render () {
		return $this->renderModules($this->rootNode);
	}

	/**
	 * Render all $modules
	 *
	 * @param array $modules
	 */
	private function renderModules ($modules) {
		$page	= '';
		$i		= 0;

		foreach ($modules as $module) {
			# Modules may contain modules
			$childModules = false;

			# If this one does, render children first
			if ($module->hasChildNodes()) {
				$childModules = $this->renderModules($module->childNodes);
			}

			# We need to differentiate wrappers and modules
			$isWrapper = strtolower($module->nodeName) == 'wrapper' ? true : false;

			# Wrappers don't have templates, a wrapper only contains child-modules
			$moduleTpl = $isWrapper ? false : $this->renderModule($module->nodeName, array('child_modules' => $childModules));

			# Only continue if we have a module template or child modules
			if ($moduleTpl or $childModules or $isWrapper) {
				# Wrappers get their HTML-IDs from their name-attributes
				# whereas modules get them from the actual node name
				$id = $isWrapper ? strtolower(ccFix($module->getAttribute('name'))) : strtolower(ccFix($module->nodeName));

				# Work out the type of wrapping element (default: section (default for wrappers: div))
				# The type can be overriden on a page per page basis using the type-attribute (eg <Mod type="nav"/>)
				$el = $module->getAttribute('type');

				# Type not defined in xml page - go to default (or as defined by module.info-file)
				if (!$el) {
					if ($isWrapper) {
						$el = 'div';
					}
					else {
						$el = 'section';

						# See if this module has set its type through its .info-file
						$infoPath = SleekPHP::site()->getModuleInfoPath($module->nodeName);

						if ($infoPath) {
							$moduleInfo = getComponentData($module->nodeName, dirname($infoPath), 'Modules');

							if (isset($moduleInfo['type'])) {
								$el = $moduleInfo['type'];
							}
						}
					}
				}

				# Wrap all modules and wrappers in $el's (except Base)
				# Create the opening $el-tag:
				if ($module->nodeName != 'Base' and $el != 'none') {
					$page .= "\n\n<$el id=\"$id\">";
				}

				# If it's a wrapper just append the child-modules
				if ($isWrapper) {
					$page .= Markdown($module->getAttribute('content')) . $childModules;
				}
				# If it's a real module append (potential) module template
				else {
					$page .= $moduleTpl;
				}

				# Close the $el
				if ($module->nodeName != 'Base' and $el != 'none') {
					$page .= "</$el>";
				}
			}
		}

		return $page;
	}

	/**
	 * Passes $extraData (and other data) to $module and then renders it
	 *
	 * @param string $module
	 * @param array $extraData
	 */
	private function renderModule ($module, array $extraData) {
		$sites		= SleekPHP::site()->getSites();
		$view		= null;
		$data		= $extraData;
		$before		= false;
		$middle		= false;
		$after		= false;

		# If this module has a class - find out which view it wants to run and grab its data
		if (isset(SleekPHP::$modules[$module])) {
			$view = SleekPHP::$modules[$module]->getView();
			$data = array_merge($data, (array)SleekPHP::$modules[$module]->getData());
		}
		# No module class - view name == module name
		else {
			$view = $module;
		}

		# If $view == false don't display anything
		if ($view === false) {
			return '';
		}

		# Now that we have the view name, go through all sites 
		# and get Before, Middle and After-templates
		# TODO: Should be moved to Site? $site->getModuleViewPaths('RecentComments') = array('before', 'middle', 'after')
		foreach ($sites as $site) {
			$beforePath	= DOCROOT ."Sites/$site/Modules/$module/Before{$view}.tpl.php";
			$middlePath	= DOCROOT ."Sites/$site/Modules/$module/$view.tpl.php";
			$afterPath	= DOCROOT ."Sites/$site/Modules/$module/After{$view}.tpl.php";

			if ($before === false and file_exists($beforePath)) {
				$before = $this->fetchTpl($beforePath, $data);

				Logger::log("Fetched $module before view \"" . basename($beforePath) . "\" from site \"$site\"", __METHOD__);
			}
			if ($middle === false and file_exists($middlePath)) {
				$middle = $this->fetchTpl($middlePath, $data);

				Logger::log("Fetched $module middle view \"" . basename($middlePath) . "\" from site \"$site\"", __METHOD__);
			}
			if ($after === false and file_exists($afterPath)) {
				$after = $this->fetchTpl($afterPath, $data);

				Logger::log("Fetched $module after view \"" . basename($afterPath) . "\" from site \"$site\"", __METHOD__);
			}

			if ($before and $middle and $after) {
				break;
			}
		}

		$all = $before . $middle . $after;

		# If $child_modules is set (could be several modules) and $all doesn't contain
		# it that means none of the module's template echo:ed $child_modules, append them autoamtically
		if (isset($data['child_modules']) and !empty($data['child_modules']) and false === strpos($all, $data['child_modules'])) {
			$all .= $data['child_modules'];

			Logger::log('$child_modules have been automatically appended to the module view.', __METHOD__);
		}

		if (!$all) {
			Logger::log("Module \"$module\" does not have any view.", __METHOD__, Logger::WARNING);
		}

		return $all ? $all : false;
	}

	/**
	 * Passes $vars to $tpl and then fetches $tpl
	 *
	 * @param string $tpl
	 * @param array $vars
	 */
	private static function fetchTpl ($tpl, $vars) {
		ob_start();

		$__all = $vars;

		extract((array)$vars);

		ini_set('display_errors', false);

		include $tpl;

		if (DEBUG) {
			ini_set('display_errors', true);
		}

		$contents = ob_get_contents();

		ob_end_clean();

		return $contents;
	}
}
